// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package views

import (
	"errors"
	"fmt"
	"sync"
	"testing"
	"testing/synctest"
	"time"

	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/hashicorp/terraform/internal/terminal"
	"github.com/hashicorp/terraform/internal/terraform"
)

func testJSONHookResourceID(addr addrs.AbsResourceInstance) terraform.HookResourceIdentity {
	return terraform.HookResourceIdentity{
		Addr: addr,
		ProviderAddr: addrs.Provider{
			Type:      "test",
			Namespace: "hashicorp",
			Hostname:  "example.com",
		},
	}
}

func testJSONLifecycleHook(actionAddr addrs.AbsActionInstance, triggeringResourceAddr addrs.AbsResourceInstance, actionTriggerIndex int, actionsListIndex int) terraform.HookActionIdentity {
	return terraform.HookActionIdentity{
		Addr: actionAddr,
		ActionTrigger: &plans.LifecycleActionTrigger{
			TriggeringResourceAddr:  triggeringResourceAddr,
			ActionTriggerBlockIndex: actionTriggerIndex,
			ActionsListIndex:        actionsListIndex,
			ActionTriggerEvent:      configs.AfterCreate,
		},
	}
}

func testJSONInvokeHook(actionAddr addrs.AbsActionInstance) terraform.HookActionIdentity {
	return terraform.HookActionIdentity{
		Addr:          actionAddr,
		ActionTrigger: new(plans.InvokeActionTrigger),
	}
}

// Test a sequence of hooks associated with creating a resource
func TestJSONHook_create(t *testing.T) {
	streams, done := terminal.StreamsForTesting(t)
	hook := newJSONHook(NewJSONView(NewView(streams)))

	var nowMu sync.Mutex
	now := time.Now()
	hook.timeNow = func() time.Time {
		nowMu.Lock()
		defer nowMu.Unlock()
		return now
	}

	after := make(chan time.Time, 1)
	hook.timeAfter = func(time.Duration) <-chan time.Time { return after }

	addr := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test_instance",
		Name: "boop",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
	priorState := cty.NullVal(cty.Object(map[string]cty.Type{
		"id":  cty.String,
		"bar": cty.List(cty.String),
	}))
	plannedNewState := cty.ObjectVal(map[string]cty.Value{
		"id": cty.StringVal("test"),
		"bar": cty.ListVal([]cty.Value{
			cty.StringVal("baz"),
		}),
	})

	action, err := hook.PreApply(testJSONHookResourceID(addr), addrs.NotDeposed, plans.Create, priorState, plannedNewState)
	testHookReturnValues(t, action, err)

	action, err = hook.PreProvisionInstanceStep(testJSONHookResourceID(addr), "local-exec")
	testHookReturnValues(t, action, err)

	hook.ProvisionOutput(testJSONHookResourceID(addr), "local-exec", `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`)

	action, err = hook.PostProvisionInstanceStep(testJSONHookResourceID(addr), "local-exec", nil)
	testHookReturnValues(t, action, err)

	// Travel 10s into the future, notify the progress goroutine, and sleep
	// briefly to allow it to execute
	nowMu.Lock()
	now = now.Add(10 * time.Second)
	after <- now
	nowMu.Unlock()
	time.Sleep(10 * time.Millisecond)

	// Travel 10s into the future, notify the progress goroutine, and sleep
	// briefly to allow it to execute
	nowMu.Lock()
	now = now.Add(10 * time.Second)
	after <- now
	nowMu.Unlock()
	time.Sleep(10 * time.Millisecond)

	// Travel 2s into the future. We have arrived!
	nowMu.Lock()
	now = now.Add(2 * time.Second)
	nowMu.Unlock()

	action, err = hook.PostApply(testJSONHookResourceID(addr), addrs.NotDeposed, plannedNewState, nil)
	testHookReturnValues(t, action, err)

	// Shut down the progress goroutine if still active
	hook.resourceProgressMu.Lock()
	for key, progress := range hook.resourceProgress {
		close(progress.done)
		<-progress.heartbeatDone
		delete(hook.resourceProgress, key)
	}
	hook.resourceProgressMu.Unlock()

	wantResource := map[string]interface{}{
		"addr":             string("test_instance.boop"),
		"implied_provider": string("test"),
		"module":           string(""),
		"resource":         string("test_instance.boop"),
		"resource_key":     nil,
		"resource_name":    string("boop"),
		"resource_type":    string("test_instance"),
	}
	want := []map[string]interface{}{
		{
			"@level":   "info",
			"@message": "test_instance.boop: Creating...",
			"@module":  "terraform.ui",
			"type":     "apply_start",
			"hook": map[string]interface{}{
				"action":   string("create"),
				"resource": wantResource,
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop: Provisioning with 'local-exec'...",
			"@module":  "terraform.ui",
			"type":     "provision_start",
			"hook": map[string]interface{}{
				"provisioner": "local-exec",
				"resource":    wantResource,
			},
		},
		{
			"@level":   "info",
			"@message": `test_instance.boop: (local-exec): Executing: ["/bin/sh" "-c" "touch /etc/motd"]`,
			"@module":  "terraform.ui",
			"type":     "provision_progress",
			"hook": map[string]interface{}{
				"output":      `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`,
				"provisioner": "local-exec",
				"resource":    wantResource,
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop: (local-exec) Provisioning complete",
			"@module":  "terraform.ui",
			"type":     "provision_complete",
			"hook": map[string]interface{}{
				"provisioner": "local-exec",
				"resource":    wantResource,
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop: Still creating... [10s elapsed]",
			"@module":  "terraform.ui",
			"type":     "apply_progress",
			"hook": map[string]interface{}{
				"action":          string("create"),
				"elapsed_seconds": float64(10),
				"resource":        wantResource,
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop: Still creating... [20s elapsed]",
			"@module":  "terraform.ui",
			"type":     "apply_progress",
			"hook": map[string]interface{}{
				"action":          string("create"),
				"elapsed_seconds": float64(20),
				"resource":        wantResource,
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop: Creation complete after 22s [id=test]",
			"@module":  "terraform.ui",
			"type":     "apply_complete",
			"hook": map[string]interface{}{
				"action":          string("create"),
				"elapsed_seconds": float64(22),
				"id_key":          "id",
				"id_value":        "test",
				"resource":        wantResource,
			},
		},
	}

	testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func TestJSONHook_errors(t *testing.T) {
	streams, done := terminal.StreamsForTesting(t)
	hook := newJSONHook(NewJSONView(NewView(streams)))

	addr := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test_instance",
		Name: "boop",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
	priorState := cty.NullVal(cty.Object(map[string]cty.Type{
		"id":  cty.String,
		"bar": cty.List(cty.String),
	}))
	plannedNewState := cty.ObjectVal(map[string]cty.Value{
		"id": cty.StringVal("test"),
		"bar": cty.ListVal([]cty.Value{
			cty.StringVal("baz"),
		}),
	})

	action, err := hook.PreApply(testJSONHookResourceID(addr), addrs.NotDeposed, plans.Delete, priorState, plannedNewState)
	testHookReturnValues(t, action, err)

	provisionError := fmt.Errorf("provisioner didn't want to")
	action, err = hook.PostProvisionInstanceStep(testJSONHookResourceID(addr), "local-exec", provisionError)
	testHookReturnValues(t, action, err)

	applyError := fmt.Errorf("provider was sad")
	action, err = hook.PostApply(testJSONHookResourceID(addr), addrs.NotDeposed, plannedNewState, applyError)
	testHookReturnValues(t, action, err)

	// Shut down the progress goroutine
	hook.resourceProgressMu.Lock()
	for key, progress := range hook.resourceProgress {
		close(progress.done)
		<-progress.heartbeatDone
		delete(hook.resourceProgress, key)
	}
	hook.resourceProgressMu.Unlock()

	wantResource := map[string]interface{}{
		"addr":             string("test_instance.boop"),
		"implied_provider": string("test"),
		"module":           string(""),
		"resource":         string("test_instance.boop"),
		"resource_key":     nil,
		"resource_name":    string("boop"),
		"resource_type":    string("test_instance"),
	}
	want := []map[string]interface{}{
		{
			"@level":   "info",
			"@message": "test_instance.boop: Destroying...",
			"@module":  "terraform.ui",
			"type":     "apply_start",
			"hook": map[string]interface{}{
				"action":   string("delete"),
				"resource": wantResource,
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop: (local-exec) Provisioning errored",
			"@module":  "terraform.ui",
			"type":     "provision_errored",
			"hook": map[string]interface{}{
				"provisioner": "local-exec",
				"resource":    wantResource,
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop: Destruction errored after 0s",
			"@module":  "terraform.ui",
			"type":     "apply_errored",
			"hook": map[string]interface{}{
				"action":          string("delete"),
				"elapsed_seconds": float64(0),
				"resource":        wantResource,
			},
		},
	}

	testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func TestJSONHook_refresh(t *testing.T) {
	streams, done := terminal.StreamsForTesting(t)
	hook := newJSONHook(NewJSONView(NewView(streams)))

	addr := addrs.Resource{
		Mode: addrs.DataResourceMode,
		Type: "test_data_source",
		Name: "beep",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
	state := cty.ObjectVal(map[string]cty.Value{
		"id": cty.StringVal("honk"),
		"bar": cty.ListVal([]cty.Value{
			cty.StringVal("baz"),
		}),
	})

	action, err := hook.PreRefresh(testJSONHookResourceID(addr), addrs.NotDeposed, state)
	testHookReturnValues(t, action, err)

	action, err = hook.PostRefresh(testJSONHookResourceID(addr), addrs.NotDeposed, state, state)
	testHookReturnValues(t, action, err)

	wantResource := map[string]interface{}{
		"addr":             string("data.test_data_source.beep"),
		"implied_provider": string("test"),
		"module":           string(""),
		"resource":         string("data.test_data_source.beep"),
		"resource_key":     nil,
		"resource_name":    string("beep"),
		"resource_type":    string("test_data_source"),
	}
	want := []map[string]interface{}{
		{
			"@level":   "info",
			"@message": "data.test_data_source.beep: Refreshing state... [id=honk]",
			"@module":  "terraform.ui",
			"type":     "refresh_start",
			"hook": map[string]interface{}{
				"resource": wantResource,
				"id_key":   "id",
				"id_value": "honk",
			},
		},
		{
			"@level":   "info",
			"@message": "data.test_data_source.beep: Refresh complete [id=honk]",
			"@module":  "terraform.ui",
			"type":     "refresh_complete",
			"hook": map[string]interface{}{
				"resource": wantResource,
				"id_key":   "id",
				"id_value": "honk",
			},
		},
	}

	testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func TestJSONHook_EphemeralOp(t *testing.T) {
	streams, done := terminal.StreamsForTesting(t)
	hook := newJSONHook(NewJSONView(NewView(streams)))

	addr := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test_instance",
		Name: "boop",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

	action, err := hook.PreEphemeralOp(testJSONHookResourceID(addr), plans.Open)
	testHookReturnValues(t, action, err)

	action, err = hook.PostEphemeralOp(testJSONHookResourceID(addr), plans.Open, nil)
	testHookReturnValues(t, action, err)

	want := []map[string]interface{}{
		{
			"@level":   "info",
			"@message": "test_instance.boop: Opening...",
			"@module":  "terraform.ui",
			"type":     "ephemeral_op_start",
			"hook": map[string]interface{}{
				"action": string("open"),
				"resource": map[string]interface{}{
					"addr":             string("test_instance.boop"),
					"implied_provider": string("test"),
					"module":           string(""),
					"resource":         string("test_instance.boop"),
					"resource_key":     nil,
					"resource_name":    string("boop"),
					"resource_type":    string("test_instance"),
				},
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop: Opening complete after 0s",
			"@module":  "terraform.ui",
			"type":     "ephemeral_op_complete",
			"hook": map[string]interface{}{
				"action":          string("open"),
				"elapsed_seconds": float64(0),
				"resource": map[string]interface{}{
					"addr":             string("test_instance.boop"),
					"implied_provider": string("test"),
					"module":           string(""),
					"resource":         string("test_instance.boop"),
					"resource_key":     nil,
					"resource_name":    string("boop"),
					"resource_type":    string("test_instance"),
				},
			},
		},
	}

	testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func TestJSONHook_EphemeralOp_progress(t *testing.T) {
	syncTest, streams, done := streamableSyncTest(t)

	syncTest(t, func(t *testing.T) {
		start := time.Now()
		hook := newJSONHook(NewJSONView(NewView(streams)))
		hook.periodicUiTimer = 1 * time.Second
		t.Log(time.Since(start))

		addr := addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_instance",
			Name: "boop",
		}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

		action, err := hook.PreEphemeralOp(testJSONHookResourceID(addr), plans.Open)
		testHookReturnValues(t, action, err)
		time.Sleep(2005 * time.Millisecond)

		action, err = hook.PostEphemeralOp(testJSONHookResourceID(addr), plans.Open, nil)
		testHookReturnValues(t, action, err)

		want := []map[string]interface{}{
			{
				"@level":   "info",
				"@message": "test_instance.boop: Opening...",
				"@module":  "terraform.ui",
				"type":     "ephemeral_op_start",
				"hook": map[string]interface{}{
					"action": string("open"),
					"resource": map[string]interface{}{
						"addr":             string("test_instance.boop"),
						"implied_provider": string("test"),
						"module":           string(""),
						"resource":         string("test_instance.boop"),
						"resource_key":     nil,
						"resource_name":    string("boop"),
						"resource_type":    string("test_instance"),
					},
				},
			},
			{
				"@level":   "info",
				"@message": "test_instance.boop: Still opening... [1s elapsed]",
				"@module":  "terraform.ui",
				"type":     "ephemeral_op_progress",
				"hook": map[string]interface{}{
					"action":          string("open"),
					"elapsed_seconds": float64(1),
					"resource": map[string]interface{}{
						"addr":             string("test_instance.boop"),
						"implied_provider": string("test"),
						"module":           string(""),
						"resource":         string("test_instance.boop"),
						"resource_key":     nil,
						"resource_name":    string("boop"),
						"resource_type":    string("test_instance"),
					},
				},
			},
			{
				"@level":   "info",
				"@message": "test_instance.boop: Still opening... [2s elapsed]",
				"@module":  "terraform.ui",
				"type":     "ephemeral_op_progress",
				"hook": map[string]interface{}{
					"action":          string("open"),
					"elapsed_seconds": float64(2),
					"resource": map[string]interface{}{
						"addr":             string("test_instance.boop"),
						"implied_provider": string("test"),
						"module":           string(""),
						"resource":         string("test_instance.boop"),
						"resource_key":     nil,
						"resource_name":    string("boop"),
						"resource_type":    string("test_instance"),
					},
				},
			},
			{
				"@level":   "info",
				"@message": "test_instance.boop: Opening complete after 2s",
				"@module":  "terraform.ui",
				"type":     "ephemeral_op_complete",
				"hook": map[string]interface{}{
					"action":          string("open"),
					"elapsed_seconds": float64(2),
					"resource": map[string]interface{}{
						"addr":             string("test_instance.boop"),
						"implied_provider": string("test"),
						"module":           string(""),
						"resource":         string("test_instance.boop"),
						"resource_key":     nil,
						"resource_name":    string("boop"),
						"resource_type":    string("test_instance"),
					},
				},
			},
		}
		stdout := done(t).Stdout()
		testJSONViewOutputEquals(t, stdout, want)
	})
}

func TestJSONHook_EphemeralOp_error(t *testing.T) {
	streams, done := terminal.StreamsForTesting(t)
	hook := newJSONHook(NewJSONView(NewView(streams)))

	addr := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test_instance",
		Name: "boop",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

	action, err := hook.PreEphemeralOp(testJSONHookResourceID(addr), plans.Open)
	testHookReturnValues(t, action, err)

	action, err = hook.PostEphemeralOp(testJSONHookResourceID(addr), plans.Open, errors.New("test error"))
	testHookReturnValues(t, action, err)

	want := []map[string]interface{}{
		{
			"@level":   "info",
			"@message": "test_instance.boop: Opening...",
			"@module":  "terraform.ui",
			"type":     "ephemeral_op_start",
			"hook": map[string]interface{}{
				"action": string("open"),
				"resource": map[string]interface{}{
					"addr":             string("test_instance.boop"),
					"implied_provider": string("test"),
					"module":           string(""),
					"resource":         string("test_instance.boop"),
					"resource_key":     nil,
					"resource_name":    string("boop"),
					"resource_type":    string("test_instance"),
				},
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop: Opening errored after 0s",
			"@module":  "terraform.ui",
			"type":     "ephemeral_op_errored",
			"hook": map[string]interface{}{
				"action":          string("open"),
				"elapsed_seconds": float64(0),
				"resource": map[string]interface{}{
					"addr":             string("test_instance.boop"),
					"implied_provider": string("test"),
					"module":           string(""),
					"resource":         string("test_instance.boop"),
					"resource_key":     nil,
					"resource_name":    string("boop"),
					"resource_type":    string("test_instance"),
				},
			},
		},
	}

	testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func TestJSONHook_actions(t *testing.T) {
	streams, done := terminal.StreamsForTesting(t)
	hook := newJSONHook(NewJSONView(NewView(streams)))

	actionA := addrs.AbsActionInstance{
		Module: addrs.RootModuleInstance,
		Action: addrs.Action{
			Type: "aws_lambda_invocation",
			Name: "notify_slack",
		}.Instance(addrs.IntKey(42)),
	}

	resourceA := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test_instance",
		Name: "boop",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

	subModule := addrs.RootModuleInstance.Child("childMod", addrs.StringKey("infra"))
	actionB := addrs.AbsActionInstance{
		Module: subModule,
		Action: addrs.Action{
			Type: "ansible_playbook",
			Name: "webserver",
		}.Instance(addrs.NoKey),
	}

	resourceB := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test_instance",
		Name: "boop",
	}.Instance(addrs.NoKey).Absolute(subModule)

	actionC := addrs.AbsActionInstance{
		Module: addrs.RootModuleInstance,
		Action: addrs.Action{
			Type: "aws_lambda_invocation",
			Name: "invoke_me",
		}.Instance(addrs.IntKey(42)),
	}

	actionAHookId := testJSONLifecycleHook(actionA, resourceA, 0, 1)
	actionBHookId := testJSONLifecycleHook(actionB, resourceB, 2, 3)
	actionCHookId := testJSONInvokeHook(actionC)

	action, err := hook.StartAction(actionAHookId)
	testHookReturnValues(t, action, err)

	action, err = hook.ProgressAction(actionAHookId, "Hello world from the lambda function")
	testHookReturnValues(t, action, err)

	action, err = hook.StartAction(actionBHookId)
	testHookReturnValues(t, action, err)

	action, err = hook.ProgressAction(actionBHookId, "TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]")
	testHookReturnValues(t, action, err)

	action, err = hook.CompleteAction(actionBHookId, nil)
	testHookReturnValues(t, action, err)

	action, err = hook.CompleteAction(actionAHookId, errors.New("lambda terminated with exit code 1"))
	testHookReturnValues(t, action, err)

	action, err = hook.StartAction(actionCHookId)
	testHookReturnValues(t, action, err)

	action, err = hook.ProgressAction(actionCHookId, "Hello world from the invoked action")
	testHookReturnValues(t, action, err)

	action, err = hook.CompleteAction(actionCHookId, nil)
	testHookReturnValues(t, action, err)

	want := []map[string]interface{}{
		{
			"@level":   "info",
			"@message": "test_instance.boop.trigger[0]: Action started: action.aws_lambda_invocation.notify_slack[42]",
			"@module":  "terraform.ui",
			"type":     "action_start",
			"hook": map[string]interface{}{
				"action": map[string]interface{}{
					"addr":             "action.aws_lambda_invocation.notify_slack[42]",
					"module":           "",
					"implied_provider": "aws",
					"action":           "action.aws_lambda_invocation.notify_slack[42]",
					"action_key":       float64(42),
					"action_name":      "notify_slack",
					"action_type":      "aws_lambda_invocation",
				},
				"lifecycle": map[string]interface{}{
					"actions_index": float64(1),
					"resource": map[string]interface{}{
						"addr":             "test_instance.boop",
						"implied_provider": "test",
						"module":           "",
						"resource":         "test_instance.boop",
						"resource_key":     nil,
						"resource_name":    "boop",
						"resource_type":    "test_instance",
					},
					"trigger_event": "AfterCreate",
					"trigger_index": float64(0),
				},
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop (0): action.aws_lambda_invocation.notify_slack[42] - Hello world from the lambda function",
			"@module":  "terraform.ui",
			"type":     "action_progress",
			"hook": map[string]interface{}{
				"action": map[string]interface{}{
					"addr":             "action.aws_lambda_invocation.notify_slack[42]",
					"module":           "",
					"implied_provider": "aws",
					"action":           "action.aws_lambda_invocation.notify_slack[42]",
					"action_key":       float64(42),
					"action_name":      "notify_slack",
					"action_type":      "aws_lambda_invocation",
				},
				"message": "Hello world from the lambda function",
				"lifecycle": map[string]interface{}{
					"actions_index": float64(1),
					"resource": map[string]interface{}{
						"addr":             "test_instance.boop",
						"implied_provider": "test",
						"module":           "",
						"resource":         "test_instance.boop",
						"resource_key":     nil,
						"resource_name":    "boop",
						"resource_type":    "test_instance",
					},
					"trigger_event": "AfterCreate",
					"trigger_index": float64(0),
				},
			},
		},
		{
			"@level":   "info",
			"@message": "module.childMod[\"infra\"].test_instance.boop.trigger[2]: Action started: module.childMod[\"infra\"].action.ansible_playbook.webserver",
			"@module":  "terraform.ui",
			"type":     "action_start",
			"hook": map[string]interface{}{
				"action": map[string]interface{}{
					"addr":             "module.childMod[\"infra\"].action.ansible_playbook.webserver",
					"module":           "module.childMod[\"infra\"]",
					"implied_provider": "ansible",
					"action":           "action.ansible_playbook.webserver",
					"action_key":       nil,
					"action_name":      "webserver",
					"action_type":      "ansible_playbook",
				},
				"lifecycle": map[string]interface{}{
					"actions_index": float64(3),
					"resource": map[string]interface{}{
						"addr":             "module.childMod[\"infra\"].test_instance.boop",
						"implied_provider": "test",
						"module":           "module.childMod[\"infra\"]",
						"resource":         "test_instance.boop",
						"resource_key":     nil,
						"resource_name":    "boop",
						"resource_type":    "test_instance",
					},
					"trigger_event": "AfterCreate",
					"trigger_index": float64(2),
				},
			},
		},
		{
			"@level":   "info",
			"@message": "module.childMod[\"infra\"].test_instance.boop (2): module.childMod[\"infra\"].action.ansible_playbook.webserver - TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]",
			"@module":  "terraform.ui",
			"type":     "action_progress",
			"hook": map[string]interface{}{
				"action": map[string]interface{}{
					"addr":             "module.childMod[\"infra\"].action.ansible_playbook.webserver",
					"module":           "module.childMod[\"infra\"]",
					"implied_provider": "ansible",
					"action":           "action.ansible_playbook.webserver",
					"action_key":       nil,
					"action_name":      "webserver",
					"action_type":      "ansible_playbook",
				},
				"message": "TASK: [hello]\n ok: [localhost] => (item=Hello world from the ansible playbook]",
				"lifecycle": map[string]interface{}{
					"actions_index": float64(3),
					"resource": map[string]interface{}{
						"addr":             "module.childMod[\"infra\"].test_instance.boop",
						"implied_provider": "test",
						"module":           "module.childMod[\"infra\"]",
						"resource":         "test_instance.boop",
						"resource_key":     nil,
						"resource_name":    "boop",
						"resource_type":    "test_instance",
					},
					"trigger_event": "AfterCreate",
					"trigger_index": float64(2),
				},
			},
		},
		{
			"@level":   "info",
			"@message": "module.childMod[\"infra\"].test_instance.boop (2): Action complete: module.childMod[\"infra\"].action.ansible_playbook.webserver",
			"@module":  "terraform.ui",
			"type":     "action_complete",
			"hook": map[string]interface{}{
				"action": map[string]interface{}{
					"addr":             "module.childMod[\"infra\"].action.ansible_playbook.webserver",
					"module":           "module.childMod[\"infra\"]",
					"implied_provider": "ansible",
					"action":           "action.ansible_playbook.webserver",
					"action_key":       nil,
					"action_name":      "webserver",
					"action_type":      "ansible_playbook",
				},
				"lifecycle": map[string]interface{}{
					"actions_index": float64(3),
					"resource": map[string]interface{}{
						"addr":             "module.childMod[\"infra\"].test_instance.boop",
						"implied_provider": "test",
						"module":           "module.childMod[\"infra\"]",
						"resource":         "test_instance.boop",
						"resource_key":     nil,
						"resource_name":    "boop",
						"resource_type":    "test_instance",
					},
					"trigger_event": "AfterCreate",
					"trigger_index": float64(2),
				},
			},
		},
		{
			"@level":   "info",
			"@message": "test_instance.boop (0): Action errored: action.aws_lambda_invocation.notify_slack[42] - lambda terminated with exit code 1",
			"@module":  "terraform.ui",
			"type":     "action_errored",
			"hook": map[string]interface{}{
				"action": map[string]interface{}{
					"addr":             "action.aws_lambda_invocation.notify_slack[42]",
					"module":           "",
					"implied_provider": "aws",
					"action":           "action.aws_lambda_invocation.notify_slack[42]",
					"action_key":       float64(42),
					"action_name":      "notify_slack",
					"action_type":      "aws_lambda_invocation",
				},
				"error": "lambda terminated with exit code 1",
				"lifecycle": map[string]interface{}{
					"actions_index": float64(1),
					"resource": map[string]interface{}{
						"addr":             "test_instance.boop",
						"implied_provider": "test",
						"module":           "",
						"resource":         "test_instance.boop",
						"resource_key":     nil,
						"resource_name":    "boop",
						"resource_type":    "test_instance",
					},
					"trigger_event": "AfterCreate",
					"trigger_index": float64(0),
				},
			},
		},
		{
			"@level":   "info",
			"@message": "Action started: action.aws_lambda_invocation.invoke_me[42]",
			"@module":  "terraform.ui",
			"type":     "action_start",
			"hook": map[string]interface{}{
				"action": map[string]interface{}{
					"addr":             "action.aws_lambda_invocation.invoke_me[42]",
					"module":           "",
					"implied_provider": "aws",
					"action":           "action.aws_lambda_invocation.invoke_me[42]",
					"action_key":       float64(42),
					"action_name":      "invoke_me",
					"action_type":      "aws_lambda_invocation",
				},
				"invoke": map[string]interface{}{},
			},
		},
		{
			"@level":   "info",
			"@message": "action.aws_lambda_invocation.invoke_me[42] - Hello world from the invoked action",
			"@module":  "terraform.ui",
			"type":     "action_progress",
			"hook": map[string]interface{}{
				"action": map[string]interface{}{
					"addr":             "action.aws_lambda_invocation.invoke_me[42]",
					"module":           "",
					"implied_provider": "aws",
					"action":           "action.aws_lambda_invocation.invoke_me[42]",
					"action_key":       float64(42),
					"action_name":      "invoke_me",
					"action_type":      "aws_lambda_invocation",
				},
				"message": "Hello world from the invoked action",
				"invoke":  map[string]interface{}{},
			},
		},
		{
			"@level":   "info",
			"@message": "Action complete: action.aws_lambda_invocation.invoke_me[42]",
			"@module":  "terraform.ui",
			"type":     "action_complete",
			"hook": map[string]interface{}{
				"action": map[string]interface{}{
					"addr":             "action.aws_lambda_invocation.invoke_me[42]",
					"module":           "",
					"implied_provider": "aws",
					"action":           "action.aws_lambda_invocation.invoke_me[42]",
					"action_key":       float64(42),
					"action_name":      "invoke_me",
					"action_type":      "aws_lambda_invocation",
				},
				"invoke": map[string]interface{}{},
			},
		},
	}

	testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func testHookReturnValues(t *testing.T, action terraform.HookAction, err error) {
	t.Helper()

	if err != nil {
		t.Fatal(err)
	}
	if action != terraform.HookActionContinue {
		t.Fatalf("Expected hook to continue, given: %#v", action)
	}
}

// streamableSyncTest is a helper to ensure that the long-running streaming goroutines are started outside of the synctest bubble.
// Otherwise, the sync bubble will be unable to advance time, and the main goroutine will become infinitely paused on any time.Sleep operation.
func streamableSyncTest(t *testing.T) (func(t *testing.T, f func(*testing.T)), *terminal.Streams, func(*testing.T) *terminal.TestOutput) {
	streams, done := terminal.StreamsForTesting(t)
	return synctest.Test, streams, done
}
